﻿namespace CodeSHARPer.View
{
    using CodeSHARPer.AvalonEdit;
    using CodeSHARPer.ViewModel;
    using ICSharpCode.AvalonEdit.Editing;
    using ICSharpCode.AvalonEdit.Folding;
    using ICSharpCode.AvalonEdit.Highlighting;
    using ICSharpCode.AvalonEdit.Rendering;
    using ICSharpCode.CodeCompletion;
    using System;
    using System.Linq;
    using System.CodeDom.Compiler;
    using System.Net;
    using System.Reflection;
    using System.ServiceModel;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Media;
    using System.Windows.Threading;

    /// <summary>
    /// Interaction logic for Document.xaml
    /// </summary>
    public partial class CodePadView : UserControl, ICodePadView
    {
        #region fields
        // Dependency property that can be set on this control and be forwarded to the textEditor control inside
        private static readonly DependencyProperty SyntaxHighlightingProperty =
          DependencyProperty.Register("SyntaxHighlighting",
                                      typeof(IHighlightingDefinition),
                                      typeof(CodePadView),
                                      new FrameworkPropertyMetadata(null,
                                      CodePadView.OnSyntaxHighlightingChanged));

        // Dependency property that can be set on this control and be forwarded to the textEditor control inside
        public static readonly DependencyProperty ShowLineNumbersProperty =
            DependencyProperty.Register("ShowLineNumbers",
                                        typeof(bool),
                                        typeof(CodePadView),
                                        new UIPropertyMetadata(false));

        // Determine whether text can be Edit or not
        private static readonly DependencyProperty IsReadOnlyProperty =
            DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(CodePadView), new UIPropertyMetadata(false));



        // Dependency property that can be set on this control and be forwarded to the textEditor control inside
        public static readonly DependencyProperty IsModifiedProperty =
            DependencyProperty.Register("IsModified", typeof(bool), typeof(CodePadView), new UIPropertyMetadata(false));

        private DispatcherTimer mFoldingUpdateTimer = null;

        #region CaretPosition
        private static readonly DependencyProperty ColumnProperty =
            DependencyProperty.Register("Column", typeof(int), typeof(CodePadView), new UIPropertyMetadata(1));


        public static readonly DependencyProperty LineProperty =
            DependencyProperty.Register("Line", typeof(int), typeof(CodePadView), new UIPropertyMetadata(1));
        #endregion CaretPosition

        #region EditorStateProperties
        /// <summary>
        /// Editor selection start
        /// </summary>
        private static readonly DependencyProperty EditorSelectionStartProperty =
            DependencyProperty.Register("EditorSelectionStart", typeof(int), typeof(CodePadView),
                                        new FrameworkPropertyMetadata(0,
                                                      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        /// <summary>
        /// Editor selection length
        /// </summary>
        private static readonly DependencyProperty EditorSelectionLengthProperty =
            DependencyProperty.Register("EditorSelectionLength", typeof(int), typeof(CodePadView),
                                        new FrameworkPropertyMetadata(0));

        /// <summary>
        /// Selected text (if any) in text Editor
        /// </summary>
        private static readonly DependencyProperty EditorSelectedTextProperty =
            DependencyProperty.Register("EditorSelectedText", typeof(string), typeof(CodePadView),
                                                new FrameworkPropertyMetadata(string.Empty));

        /// <summary>
        /// TextEditor caret position
        /// </summary>
        private static readonly DependencyProperty EditorCaretOffsetProperty =
            DependencyProperty.Register("EditorCaretOffset", typeof(int), typeof(CodePadView),
                                        new FrameworkPropertyMetadata(0));

        /// <summary>
        /// Determine whether current selection is a rectangle or not
        /// </summary>
        private static readonly DependencyProperty EditorIsRectangleSelectionProperty =
            DependencyProperty.Register("EditorIsRectangleSelection", typeof(bool), typeof(CodePadView), new UIPropertyMetadata(false));

        /// <summary>
        /// Style the background color of the current Editor line
        /// </summary>
        public SolidColorBrush EditorCurrentLineBackground
        {
            get { return (SolidColorBrush)GetValue(EditorCurrentLineBackgroundProperty); }
            set { SetValue(EditorCurrentLineBackgroundProperty, value); }
        }

        // Style the background color of the current Editor line
        private static readonly DependencyProperty EditorCurrentLineBackgroundProperty =
            DependencyProperty.Register("EditorCurrentLineBackground",
                                         typeof(SolidColorBrush),
                                         typeof(CodePadView),
                                         new UIPropertyMetadata(null, CodePadView.OnCurrentLineBackgroundChanged));

        #region EditorScrollOffsetXY
        /// <summary>
        /// Current Editor view scroll X position
        /// </summary>
        public static readonly DependencyProperty EditorScrollOffsetXProperty =
            DependencyProperty.Register("EditorScrollOffsetX", typeof(double), typeof(CodePadView), new UIPropertyMetadata(0.0));

        /// <summary>
        /// Current Editor view scroll Y position
        /// </summary>
        public static readonly DependencyProperty EditorScrollOffsetYProperty =
            DependencyProperty.Register("EditorScrollOffsetY", typeof(double), typeof(CodePadView), new UIPropertyMetadata(0.0));
        #endregion EditorScrollOffsetXY
        #endregion EditorStateProperties
        #endregion fields

        #region constructor
        public CodePadView()
        {
            this.InitializeComponent();

            this.Loaded += new RoutedEventHandler(this.OnLoaded);
            this.Unloaded += new RoutedEventHandler(this.OnUnloaded);

            // Update highlighting of current line when caret position is changed
            this.textEditor.TextArea.Caret.PositionChanged += (sender, e) =>
            {
                this.textEditor.TextArea.TextView.InvalidateLayer(KnownLayer.Background);

                if (this.textEditor.TextArea != null)
                {
                    this.Column = this.textEditor.TextArea.Caret.Column;
                    this.Line = this.textEditor.TextArea.Caret.Line;
                }
                else
                    this.Column = this.Line = 0;
            };

            //textEditor.TextArea.DefaultInputHandler.NestedInputHandlers.Add(new ICSharpCode.AvalonEdit.Search.SearchInputHandler(textEditor.TextArea));
        }
        #endregion constructor

        #region properties
        /// <summary>
        /// Dependency property that can be set on this control and be forwarded to the textEditor control inside
        /// </summary>
        public IHighlightingDefinition SyntaxHighlighting
        {
            get { return (IHighlightingDefinition)GetValue(CodePadView.SyntaxHighlightingProperty); }
            set { SetValue(CodePadView.SyntaxHighlightingProperty, value); }
        }

        /// <summary>
        /// Dependency property that can be set on this control and be forwarded to the textEditor control inside
        /// </summary>
        public bool ShowLineNumbers
        {
            get { return (bool)GetValue(ShowLineNumbersProperty); }
            set { SetValue(ShowLineNumbersProperty, value); }
        }

        /// <summary>
        /// Get/set property to determine whether text can be Edit or not
        /// </summary>
        public bool IsReadOnly
        {
            get { return (bool)GetValue(IsReadOnlyProperty); }
            set { SetValue(IsReadOnlyProperty, value); }
        }

        /// <summary>
        /// Dependency property that can be set on this control and be forwarded to the textEditor control inside
        /// </summary>
        public bool IsModified
        {
            get { return (bool)GetValue(IsModifiedProperty); }
            set { SetValue(IsModifiedProperty, value); }
        }

        #region CaretPosition
        public int Column
        {
            get { return (int)GetValue(ColumnProperty); }
            set { SetValue(ColumnProperty, value); }
        }

        public int Line
        {
            get { return (int)GetValue(LineProperty); }
            set { SetValue(LineProperty, value); }
        }
        #endregion CaretPosition

        #region EditorStateProperties
        /// <summary>
        /// Dependency property to allow ViewModel binding
        /// </summary>
        public int EditorSelectionStart
        {
            get { return (int)GetValue(CodePadView.EditorSelectionStartProperty); }
            set { SetValue(CodePadView.EditorSelectionStartProperty, value); }
        }

        /// <summary>
        /// Dependency property to allow ViewModel binding
        /// </summary>
        public int EditorSelectionLength
        {
            get { return (int)GetValue(CodePadView.EditorSelectionLengthProperty); }
            set { SetValue(CodePadView.EditorSelectionLengthProperty, value); }
        }

        /// <summary>
        /// Selected text (if any) in text Editor
        /// </summary>
        public string EditorSelectedText
        {
            get { return (string)GetValue(EditorSelectedTextProperty); }
            set { SetValue(EditorSelectedTextProperty, value); }
        }

        /// <summary>
        /// Dependency property to allow ViewModel binding
        /// </summary>
        public int EditorCaretOffset
        {
            get { return (int)GetValue(CodePadView.EditorCaretOffsetProperty); }
            set { SetValue(CodePadView.EditorCaretOffsetProperty, value); }
        }

        public bool EditorIsRectangleSelection
        {
            get { return (bool)GetValue(EditorIsRectangleSelectionProperty); }
            set { SetValue(EditorIsRectangleSelectionProperty, value); }
        }

        #region EditorScrollOffsetXY
        public double EditorScrollOffsetX
        {
            get { return (double)GetValue(EditorScrollOffsetXProperty); }
            set { SetValue(EditorScrollOffsetXProperty, value); }
        }

        public double EditorScrollOffsetY
        {
            get { return (double)GetValue(EditorScrollOffsetYProperty); }
            set { SetValue(EditorScrollOffsetYProperty, value); }
        }
        #endregion EditorScrollOffsetXY
        #endregion EditorStateProperties
        #endregion properties

        #region methods

        /// <summary>
        /// Hock event handlers and restore Editor states (if any) or defaults
        /// when the control is fully loaded.
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="args"></param>
        private void OnLoaded(object obj, RoutedEventArgs args)
        {
            // Call folding once upon loading so that first run is executed right away
            this.foldingUpdateTimer_Tick(null, null);

            this.mFoldingUpdateTimer = new DispatcherTimer();
            mFoldingUpdateTimer.Interval = TimeSpan.FromSeconds(2);
            mFoldingUpdateTimer.Tick += foldingUpdateTimer_Tick;
            mFoldingUpdateTimer.Start();

            textEditor.Focus();
            textEditor.ForceCursor = true;

            // Restore cusor position for CTRL-TAB Support http://avalondock.codeplex.com/workitem/15079
            textEditor.ScrollToHorizontalOffset(this.EditorScrollOffsetX);
            textEditor.ScrollToVerticalOffset(this.EditorScrollOffsetY);

            if (this.EditorIsRectangleSelection == true)
            {
                textEditor.CaretOffset = this.EditorCaretOffset;

                textEditor.SelectionStart = this.EditorSelectionStart;
                textEditor.SelectionLength = this.EditorSelectionLength;

                // See OnMouseCaretBoxSelection in Editing\CaretNavigationCommandHandler.cs
                // Convert normal selection to rectangle selection
                textEditor.TextArea.Selection = new ICSharpCode.AvalonEdit.Editing.RectangleSelection(textEditor.TextArea,
                                                                                                      textEditor.TextArea.Selection.StartPosition,
                                                                                                      textEditor.TextArea.Selection.EndPosition);
            }
            else
            {
                textEditor.CaretOffset = this.EditorCaretOffset;

                // http://www.codeproject.com/Articles/42490/Using-AvalonEdit-WPF-Text-Editor?msg=4388281#xx4388281xx
                textEditor.Select(this.EditorSelectionStart, this.EditorSelectionLength);
            }
        }

        /// <summary>
        /// Unhock event handlers and save Editor states (to be recovered later)
        /// when the control is unloaded.
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="args"></param>
        private void OnUnloaded(object obj, RoutedEventArgs args)
        {
            if (this.mFoldingUpdateTimer != null)
                this.mFoldingUpdateTimer = null;

            this.Loaded -= this.OnLoaded;
            this.Unloaded -= this.OnUnloaded;

            // Save cusor position for CTRL-TAB Support http://avalondock.codeplex.com/workitem/15079
            this.EditorCaretOffset = textEditor.CaretOffset;
            this.EditorSelectionStart = textEditor.SelectionStart;
            this.EditorSelectionLength = textEditor.SelectionLength;
            this.EditorIsRectangleSelection = textEditor.TextArea.Selection is RectangleSelection;

            // http://stackoverflow.com/questions/11863273/avalonEdit-how-to-get-the-top-visible-line
            this.EditorScrollOffsetX = textEditor.TextArea.TextView.ScrollOffset.X;
            this.EditorScrollOffsetY = textEditor.TextArea.TextView.ScrollOffset.Y;
        }

        /// <summary>
        /// The dependency property for has changed.
        /// Chnage the <seealso cref="SolidColorBrush"/> to be used for highlighting the current Editor line
        /// in the particular <seealso cref="CodePadView"/> control.
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        private static void OnCurrentLineBackgroundChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CodePadView view = d as CodePadView;

            if (view != null && e != null)
            {
                SolidColorBrush newValue = e.NewValue as SolidColorBrush;

                if (newValue != null)
                    view.AdjustCurrentLineBackground(newValue);
            }
        }

        #region Folding
        FoldingManager foldingManager;
        //AbstractFoldingStrategy foldingStrategy;

        private static void OnSyntaxHighlightingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CodePadView view = d as CodePadView;

            if (view == null || e == null)
                return;

            IHighlightingDefinition newValue = e.NewValue as IHighlightingDefinition;

            if (newValue != null)
            {
                view.SyntaxHighlighting = newValue;
                view.SetFolding(newValue);
            }
        }

        private bool _InstallFoldingManager = false;

        public void SetFolding(IHighlightingDefinition SyntaxHighlighting)
        {
            if (SyntaxHighlighting == null)
            {
                //foldingStrategy = null;
            }
            else
            {
                switch (SyntaxHighlighting.Name)
                {
                    case "XML":
                        //foldingStrategy = new XmlFoldingStrategy();
                        textEditor.TextArea.IndentationStrategy = new ICSharpCode.AvalonEdit.Indentation.DefaultIndentationStrategy();
                        break;
                    case "C#":
                    case "C++":
                    case "PHP":
                    case "Java":
                        textEditor.TextArea.IndentationStrategy = new ICSharpCode.AvalonEdit.Indentation.CSharp.CSharpIndentationStrategy(textEditor.Options);
                        //foldingStrategy = new BraceFoldingStrategy();
                        break;
                    default:
                        textEditor.TextArea.IndentationStrategy = new ICSharpCode.AvalonEdit.Indentation.DefaultIndentationStrategy();
                        //foldingStrategy = null;
                        break;
                }
            }

            //if (foldingStrategy != null)
            //{
            //    if (textEditor.Document != null)
            //    {
            //        if (foldingManager == null)
            //            foldingManager = FoldingManager.Install(textEditor.TextArea);

            //        foldingStrategy.UpdateFoldings(foldingManager, textEditor.Document);
            //    }
            //    else
            //        _InstallFoldingManager = true;
            //}
            //else
            //{
            //    if (foldingManager != null)
            //    {
            //        FoldingManager.Uninstall(foldingManager);
            //        foldingManager = null;
            //    }
            //}
        }

        /// <summary>
        /// Update the folding in the text Editor when requested per call on this method
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void foldingUpdateTimer_Tick(object sender, EventArgs e)
        {
            if (this.IsVisible == true)
            {
                if (_InstallFoldingManager == true)
                {
                    if (textEditor.Document != null)
                    {
                        if (foldingManager == null)
                        {
                            foldingManager = FoldingManager.Install(textEditor.TextArea);

                            _InstallFoldingManager = false;
                        }
                    }
                    else
                        return;
                }

                //if (foldingStrategy != null)
                //    foldingStrategy.UpdateFoldings(foldingManager, textEditor.Document);
            }
        }
        #endregion

        /// <summary>
        /// Reset the <seealso cref="SolidColorBrush"/> to be used for highlighting the current Editor line.
        /// </summary>
        /// <param name="newValue"></param>
        private void AdjustCurrentLineBackground(SolidColorBrush newValue)
        {
            if (newValue != null)
            {
                this.textEditor.TextArea.TextView.BackgroundRenderers.Clear();

                this.textEditor.TextArea.TextView.BackgroundRenderers.Add(
                        new HighlightCurrentLineBackgroundRenderer(this.textEditor, newValue.Clone()));
            }
        }
        #endregion methods

        public void RunCode()
        {
            Console.SetOut(new ParagraphWriter(Results));
            var task = HostConsoleServiceAsync();
            ICSharpScriptProvider cs = this.textEditor.Completion.ScriptProvider as ICSharpScriptProvider;
            CompilerResults results = cs.Compile(this.textEditor.Text);

            Results.Inlines.Clear();

            foreach (CompilerError error in results.Errors)
            {
                Run run = new Run(string.Format("Line {0}: {1}", error.Line, error.ErrorText));
                run.Foreground = error.IsWarning ? Brushes.MediumBlue : Brushes.Red;
                Results.Inlines.Add(run);
                Results.Inlines.Add(new LineBreak());
            }

            if (results.Errors.HasErrors)
            {
                Run run = new Run("(Build failed)");
                run.Foreground = Brushes.Red;
                Results.Inlines.Add(run);
                return;
            }
            else
            {
                Run run = new Run("(Build successful)");
                run.Foreground = Brushes.MediumBlue;
                Results.Inlines.Add(run);
                Results.Inlines.Add(new LineBreak());
                Results.Inlines.Add(new LineBreak());

                try
                {
                    task.Wait();
                    cs.RunCode(results);
                }
                catch (TargetInvocationException ex)
                {
                    Run error = new Run(string.Format("Exception: {0}", ex.InnerException));
                    error.Foreground = Brushes.Red;
                    Results.Inlines.Add(error);
                }
            }
        }

        private Task HostConsoleServiceAsync()
        {
            return new TaskFactory().StartNew(() => {
                try
                {
                    // Create the url that is needed to specify
                    // where the service should be started
                    string urlService = "net.pipe://" +
                        "localhost" + "/CodeSharperConsoleRedirector";

                    // Instruct the ServiceHost that the type
                    // that is used is a ServiceLibrary.service1
                    ServiceHost host = new ServiceHost(typeof(ParagraphWriter), new Uri(urlService));

                    // The binding is where we can choose what
                    // transport layer we want to use. HTTP, TCP ect.
                    NetNamedPipeBinding namedPipeBindingding = new NetNamedPipeBinding();
                    namedPipeBindingding.TransactionFlow = false;
                    namedPipeBindingding.Security.Transport.ProtectionLevel =
                       System.Net.Security.ProtectionLevel.EncryptAndSign;

                    // Add a endpoint
                    host.AddServiceEndpoint(typeof(
                       ICustomConsole), namedPipeBindingding, urlService);

                    host.Open();
                }
                catch (Exception ex)
                {
                }
            });
        }

        private void ToogleOutputVisibility(bool show)
        {
            if (show)
            {
                this.MainGrid.RowDefinitions[1].Height = new GridLength(10, GridUnitType.Star);
                this.MainGrid.RowDefinitions[2].Height = new GridLength(85, GridUnitType.Star);
            }
            else
            {
                this.MainGrid.RowDefinitions[1].Height = new GridLength(0, GridUnitType.Star);
                this.MainGrid.RowDefinitions[2].Height = new GridLength(0, GridUnitType.Star);
            }
        }

        private void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if (this.DataContext != null)
                (this.DataContext as IViewAware).View = this;
        }

        private void TabControl_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            ToogleOutputVisibility(((bool)e.NewValue));
        }
    }
}
